[STORE] abstract snapshot catalog and add Redis snapshot backend - #1739
Conversation
Introduce SerializerSnapshotStore as the snapshot catalog adapter for the existing serializer backend and route MasterService persist, restore, and cleanup flows through it instead of open-coding latest marker updates and snapshot directory scans. Also add focused unit coverage for publish/get/list/delete behavior so the snapshot catalog path can evolve independently from the payload storage path. Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refactors the snapshot management system within Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new Redis-backed snapshot catalog for the mooncake-store master service, providing an alternative to the existing serializer-backed catalog. Key changes include new Redis client helper and snapshot store implementations, updated MasterConfig with options for catalog type and connection string, and refactoring MasterService to use a generic SnapshotStore interface for snapshot management. The PersistState, CleanupOldSnapshot, and RestoreState methods have been updated to leverage this new abstraction. Review feedback suggests improving the efficiency and reducing duplication of the TrimAsciiWhitespace utility function and simplifying the MasterService::GetSnapshotStore() method by removing redundant lazy initialization logic.
| std::string TrimAsciiWhitespace(std::string value) { | ||
| while (!value.empty() && | ||
| std::isspace(static_cast<unsigned char>(value.front()))) { | ||
| value.erase(value.begin()); | ||
| } | ||
| while (!value.empty() && | ||
| std::isspace(static_cast<unsigned char>(value.back()))) { | ||
| value.pop_back(); | ||
| } | ||
| return value; | ||
| } |
There was a problem hiding this comment.
This TrimAsciiWhitespace function is inefficient because value.erase(value.begin()) has linear time complexity and is being called inside a loop. This can be optimized to have linear complexity overall by finding the first and last non-whitespace characters and creating a substring.
Additionally, this function is duplicated in mooncake-store/src/ha/serializer_snapshot_store.cpp. It would be best to move this to a shared utility file to avoid code duplication.
Here's a more efficient implementation. Since the function name is TrimAsciiWhitespace, using an explicit list of ASCII whitespace characters is more correct than the locale-dependent isspace().
std::string TrimAsciiWhitespace(std::string value) {
const auto first = value.find_first_not_of(" \t\n\r\f\v");
if (std::string::npos == first) {
return "";
}
const auto last = value.find_last_not_of(" \t\n\r\f\v");
return value.substr(first, (last - first + 1));
}There was a problem hiding this comment.
I moved the shared snapshot parsing/building helpers into a common snapshot helper so both serializer- and Redis-backed snapshot stores now use the same implementation. I also switched the trim logic to a linear first/last non-whitespace scan.
| ha::SnapshotStore* MasterService::GetSnapshotStore() { | ||
| if (!snapshot_store_ && snapshot_backend_) { | ||
| snapshot_store_ = CreateSnapshotStore(); | ||
| } | ||
| return snapshot_store_.get(); | ||
| } |
There was a problem hiding this comment.
The lazy initialization logic here seems redundant and could be confusing. The MasterService constructor already initializes snapshot_store_ via CreateSnapshotStore() right after snapshot_backend_ is created (inside the if (enable_snapshot_ || enable_snapshot_restore_) block).
Therefore, the condition !snapshot_store_ && snapshot_backend_ should never be true, making the lazy initialization appear to be dead code.
To improve clarity and remove the redundancy, you could simplify this method to just return snapshot_store_.get(). If there's a scenario where this lazy initialization is needed that I'm missing, it would be helpful to add a comment explaining it.
ha::SnapshotStore* MasterService::GetSnapshotStore() {
return snapshot_store_.get();
}There was a problem hiding this comment.
I kept the eager constructor path for the normal case, but changed GetSnapshotStore() so the fallback path is synchronized and only materializes the store when snapshot_backend_ is injected after construction. That preserves the late-bound test path without leaving an unsynchronized lazy-init branch.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR refactors snapshot catalog handling in mooncake-store behind a backend-neutral SnapshotStore interface and introduces a Redis-backed snapshot catalog implementation alongside the existing serializer-backed behavior.
Changes:
- Added
SerializerSnapshotStoreabstraction for the existing snapshot catalog marker behavior and updatedMasterServiceto use aSnapshotStore. - Implemented
RedisSnapshotStore(Publish/GetLatest/List/Delete) with shared Redis connection helpers and updated HA Redis coordinator to reuse them. - Added/updated Redis and snapshot-store tests and wired new test targets into CMake.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| mooncake-store/tests/serializer_snapshot_store_test.cpp | New unit tests for serializer-backed snapshot catalog semantics. |
| mooncake-store/tests/redis_test_utils.h | Shared Redis test helpers for HA + snapshot store tests. |
| mooncake-store/tests/redis_snapshot_store_test.cpp | New Redis snapshot catalog integration tests. |
| mooncake-store/tests/high_availability_redis_test.cpp | Switched HA Redis tests to use shared Redis helpers. |
| mooncake-store/tests/CMakeLists.txt | Added new test target(s) for snapshot store tests. |
| mooncake-store/src/serialize/serializer_backend.cpp | Added “not found” error classification for S3/local backends. |
| mooncake-store/src/master_service.cpp | Routes snapshot catalog operations through SnapshotStore + adds backend selection. |
| mooncake-store/src/master.cpp | Added CLI/config flags for snapshot catalog backend selection and connstring override. |
| mooncake-store/src/ha/serializer_snapshot_store.cpp | New serializer-backed snapshot store implementation. |
| mooncake-store/src/ha/redis_snapshot_store.cpp | New Redis-backed snapshot store implementation using Redis scripts. |
| mooncake-store/src/ha/redis_leader_coordinator.cpp | Reused shared Redis connect helpers to reduce duplicated parsing/auth/DB selection logic. |
| mooncake-store/src/ha/redis_client_helper.cpp | New shared Redis connect + parsing + key-tag utilities. |
| mooncake-store/src/CMakeLists.txt | Added newly introduced HA source files to build. |
| mooncake-store/include/serialize/serializer_backend.h | Added IsNotFoundError() hook to classify missing objects. |
| mooncake-store/include/master_service.h | Added SnapshotStore members/factory accessors in MasterService. |
| mooncake-store/include/master_config.h | Added snapshot catalog backend config fields and builder wiring. |
| mooncake-store/include/ha/serializer_snapshot_store.h | Declared SerializerSnapshotStore. |
| mooncake-store/include/ha/backends/redis/redis_snapshot_store.h | Declared RedisSnapshotStore. |
| mooncake-store/include/ha/backends/redis/redis_client_helper.h | Declared shared Redis helper APIs/types. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| auto redis = ConnectRedisForTest(FLAGS_redis_endpoint); | ||
| ASSERT_NE(redis, nullptr); | ||
| ASSERT_TRUE(redis.has_value()); | ||
| const auto master_view_key = BuildRedisMasterViewKey(cluster_namespace); | ||
| RedisReplyPtr delete_reply(static_cast<redisReply*>( | ||
| redisCommand(redis.get(), "DEL %b", master_view_key.data(), | ||
| redisCommand(redis->get(), "DEL %b", master_view_key.data(), | ||
| master_view_key.size()))); |
There was a problem hiding this comment.
ConnectRedisForTest and BuildRedisMasterViewKey were moved into mooncake::testing in redis_test_utils.h, but this call site still uses unqualified names inside namespace mooncake. This will fail to compile unless there’s a local using declaration not shown in the diff. Prefer qualifying the calls (e.g., testing::ConnectRedisForTest, testing::BuildRedisMasterViewKey) or adding explicit using mooncake::testing::...; in the local anonymous namespace.
| ${MOONCAKE_STORE_HIREDIS_INCLUDE_DIR}) | ||
| target_link_libraries(redis_snapshot_store_test | ||
| PUBLIC mooncake_store transfer_engine cachelib_memory_allocator | ||
| ${ETCD_WRAPPER_LIB} glog gflags ibverbs gtest gtest_main pthread |
There was a problem hiding this comment.
redis_snapshot_store_test.cpp defines its own main(), but the target links gtest_main, which also provides main(). This typically causes a duplicate symbol linker error. Fix by removing gtest_main from target_link_libraries(...) for this target, or removing the custom main() from the test source and letting gtest_main handle it (if you still need gflags parsing, keep the custom main() and drop gtest_main).
| ${ETCD_WRAPPER_LIB} glog gflags ibverbs gtest gtest_main pthread | |
| ${ETCD_WRAPPER_LIB} glog gflags ibverbs gtest pthread |
There was a problem hiding this comment.
Fixed in the latest update: I dropped gtest_main from the redis_snapshot_store_test target since the test already provides its own main().
|
|
||
| ha::SnapshotStore* MasterService::GetSnapshotStore() { | ||
| if (!snapshot_store_ && snapshot_backend_) { | ||
| snapshot_store_ = CreateSnapshotStore(); |
There was a problem hiding this comment.
GetSnapshotStore() lazily initializes snapshot_store_ without synchronization, which can race if multiple threads call it concurrently (writes to a std::unique_ptr are not thread-safe). A concrete fix is to guard initialization with a mutex/shared_mutex (you already have snapshot_mutex_, if appropriate), or use std::once_flag + std::call_once. Alternatively, since the constructor already sets snapshot_store_ = CreateSnapshotStore();, consider removing the lazy-init path entirely to avoid split initialization logic.
| snapshot_store_ = CreateSnapshotStore(); | |
| std::unique_lock<std::shared_mutex> lock(snapshot_mutex_); | |
| if (!snapshot_store_ && snapshot_backend_) { | |
| snapshot_store_ = CreateSnapshotStore(); | |
| } |
There was a problem hiding this comment.
I updated GetSnapshotStore() so the fallback materialization path is synchronized. The normal path is still eager construction, and the lazy path now only exists for the late-bound backend case used by tests.
| RedisReplyPtr reply(static_cast<redisReply*>(redisCommand( | ||
| context->get(), "EVAL %s 2 %b %b %b", kDeleteSnapshotScript, | ||
| index_key_.data(), index_key_.size(), latest_key_.data(), | ||
| latest_key_.size(), snapshot_id.data(), snapshot_id.size()))); | ||
| if (reply == nullptr || reply->type == REDIS_REPLY_ERROR) { | ||
| return ErrorCode::PERSISTENT_FAIL; | ||
| } | ||
|
|
||
| auto delete_result = payload_backend_->DeleteObjectsWithPrefix( | ||
| BuildSnapshotPrefix(snapshot_id)); | ||
| if (!delete_result) { | ||
| LOG(ERROR) << "Failed to delete snapshot payload after Redis catalog " | ||
| "update, snapshot_id=" | ||
| << snapshot_id << ", error=" << delete_result.error(); | ||
| return ErrorCode::PERSISTENT_FAIL; | ||
| } |
There was a problem hiding this comment.
Delete() updates the Redis catalog first and deletes payload objects second; if payload deletion fails, the catalog no longer references the payload, leaving orphaned snapshot data and making automated cleanup harder. Consider reordering (delete payload first, then update Redis) or implementing a compensating action on failure (e.g., re-add the snapshot to the ZSET and re-set latest if needed), acknowledging the tradeoff that cross-backend atomicity isn’t possible but inconsistency can be minimized.
There was a problem hiding this comment.
If payload deletion fails afterwards, we may leave orphaned payload, but reversing the order risks leaving the catalog/latest marker pointing to missing payload, which is the worse user-visible inconsistency. A follow-up GC/compensation path would be a better improvement here.
| return snapshots; | ||
| } | ||
|
|
||
| ErrorCode SerializerSnapshotStore::Delete(const SnapshotId& snapshot_id) { |
There was a problem hiding this comment.
Need check if the snapshot_id is latest
There was a problem hiding this comment.
SerializerSnapshotStore::Delete() now checks whether the removed snapshot is the current latest marker, advances it to the next snapshot when available, and clears the latest marker when deleting the last snapshot.
| } | ||
|
|
||
| ha::SnapshotStore* MasterService::GetSnapshotStore() { | ||
| if (!snapshot_store_ && snapshot_backend_) { |
There was a problem hiding this comment.
check of snapshot_backend_ here is strange. may put it somewhere else
There was a problem hiding this comment.
I tightened this path in the latest update. The normal case still uses constructor-time initialization, and GetSnapshotStore() now only falls back to synchronized late materialization when snapshot_backend_ is injected after construction. That lets us remove the old split/unsynchronized behavior while keeping the late-bound test path working.
|
|
||
| ha::SnapshotStore* MasterService::GetSnapshotStore() { | ||
| if (!snapshot_store_ && snapshot_backend_) { | ||
| snapshot_store_ = CreateSnapshotStore(); |
There was a problem hiding this comment.
Now CreateSnapshotStore is called in MasterService, why check it here
|
|
||
| #else | ||
|
|
||
| tl::expected<RedisContextPtr, ErrorCode> ConnectRedis( |
There was a problem hiding this comment.
This func is called in every api, should we make it a long connect?
There was a problem hiding this comment.
Snapshot catalog operations are not on a hot path today, and keeping per-call connections avoids introducing shared hiredis context / reconnect / threading complexity prematurely. If catalog operations become measurable in practice, connection reuse would be a reasonable follow-up.
|
|
||
| bool IsDigit(char ch) { return std::isdigit(static_cast<unsigned char>(ch)); } | ||
|
|
||
| bool IsValidSnapshotId(std::string_view snapshot_id) { |
There was a problem hiding this comment.
The duplicated snapshot helper logic is now shared instead of being implemented independently in both snapshot store backends.
| if (std::regex_search(object_key, match, state_dir_regex)) { | ||
| snapshot_dirs.insert(match[1].str()); // Extract timestamp part | ||
| } | ||
| auto list_result = snapshot_store->List(0); |
There was a problem hiding this comment.
Replaced the List(0) callsite with a named unlimited-list constant for readability.
| @@ -117,6 +117,15 @@ class SerializerBackend { | |||
| virtual tl::expected<void, std::string> ListObjectsWithPrefix( | |||
There was a problem hiding this comment.
why there are two localtions for the serialize related api
There was a problem hiding this comment.
The split is intentional here: SerializerBackend is the payload/object-store primitive layer, while SnapshotStore is the snapshot catalog semantics layer. This PR only abstracts the catalog side, so I’d prefer to keep that layering explicit instead of collapsing the two concerns in the same change.
| // or restore is enabled | ||
| // Snapshot payload storage backend type: "local" or "s3", required when | ||
| // snapshot or restore is enabled | ||
| std::string snapshot_backend_type; |
There was a problem hiding this comment.
this is only used for serial? maybe make it more abstract
There was a problem hiding this comment.
In this PR I kept snapshot_backend_type as the payload-backend knob and snapshot_catalog_backend_type as the catalog-backend knob. That seemed like the smallest change that makes the split explicit without expanding the scope into a config rename/migration.
| @@ -0,0 +1,223 @@ | |||
| #include "ha/backends/redis/redis_client_helper.h" | |||
There was a problem hiding this comment.
Both leader election and snapshot management can be offloaded to Redis. maybe isolate the snapshot directory from the leader election mechanism to decouple these two distinct responsibilities.
There was a problem hiding this comment.
The shared Redis helper here is only the low-level transport/auth/endpoint utility. The actual semantics remain separated in the leader-coordinator and snapshot-store implementations. I kept the helper shared in this PR to avoid duplicating the same connection logic in two places.
| const std::string& snapshot_id) { | ||
| try { | ||
| auto* snapshot_store = GetSnapshotStore(); | ||
| if (!snapshot_backend_ || !snapshot_store) { |
There was a problem hiding this comment.
same above. this check looks unnecessary.
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
…ache-ai#1739) * [STORE] abstract snapshot catalog in master service Introduce SerializerSnapshotStore as the snapshot catalog adapter for the existing serializer backend and route MasterService persist, restore, and cleanup flows through it instead of open-coding latest marker updates and snapshot directory scans. Also add focused unit coverage for publish/get/list/delete behavior so the snapshot catalog path can evolve independently from the payload storage path. Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] distinguish missing snapshots from backend read errors Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] add Redis snapshot catalog backend Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] share Redis connection helpers across HA backends Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> Co-authored-by: Xuchun Shang <xuchun.shang@linux.alibaba.com> * [STORE] unify Redis test helpers Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] tighten snapshot catalog state handling Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> --------- Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
…ache-ai#1739) * [STORE] abstract snapshot catalog in master service Introduce SerializerSnapshotStore as the snapshot catalog adapter for the existing serializer backend and route MasterService persist, restore, and cleanup flows through it instead of open-coding latest marker updates and snapshot directory scans. Also add focused unit coverage for publish/get/list/delete behavior so the snapshot catalog path can evolve independently from the payload storage path. Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] distinguish missing snapshots from backend read errors Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] add Redis snapshot catalog backend Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] share Redis connection helpers across HA backends Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> Co-authored-by: Xuchun Shang <xuchun.shang@linux.alibaba.com> * [STORE] unify Redis test helpers Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] tighten snapshot catalog state handling Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> --------- Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
…ache-ai#1739) * [STORE] abstract snapshot catalog in master service Introduce SerializerSnapshotStore as the snapshot catalog adapter for the existing serializer backend and route MasterService persist, restore, and cleanup flows through it instead of open-coding latest marker updates and snapshot directory scans. Also add focused unit coverage for publish/get/list/delete behavior so the snapshot catalog path can evolve independently from the payload storage path. Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] distinguish missing snapshots from backend read errors Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] add Redis snapshot catalog backend Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] share Redis connection helpers across HA backends Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> Co-authored-by: Xuchun Shang <xuchun.shang@linux.alibaba.com> * [STORE] unify Redis test helpers Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> * [STORE] tighten snapshot catalog state handling Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com> --------- Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
Description
This PR abstracts snapshot catalog handling in
mooncake-storeand adds a Redis-backed snapshot catalog implementation.Today, snapshot catalog logic is embedded in
MasterService, which makes it hard to introduce a non-etcd backend cleanly. This change extracts a backend-neutral snapshot store interface, keeps the existing serializer-backed path as one implementation, and adds a Redis snapshot catalog backend as another implementation.Module
mooncake-transfer-engine)mooncake-store)mooncake-ep)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-pg)mooncake-rl)Type of Change
What changes
MasterServiceSerializerSnapshotStoreto represent the existing snapshot catalog path explicitlyRedisSnapshotStorewith support for:PublishGetLatestListDeleteGetLatest()semantics so callers can distinguish:Non-goals
This PR does not try to solve the full hot-standby recovery story yet.
In particular, it does not introduce:
last_included_seq/producer_view_versionpopulationThose should be handled in follow-up changes together with the broader replication / recovery design.
How Has This Been Tested?
Built with Redis + etcd enabled:
cmake --build build --target redis_snapshot_store_test high_availability_test -j8Passed:
./redis_snapshot_store_test --redis_endpoint=<redis-endpoint>./high_availability_test --redis_endpoint=<redis-endpoint>On the validated setup:
redis_snapshot_store_test: 3/3 passedhigh_availability_test: 9/9 passedChecklist
./scripts/code_format.shbefore submitting.